package de.dopichaj.labrador.index.meta;



import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import de.dopichaj.labrador.index.Tag;
import de.dopichaj.labrador.index.TagFactory;
import de.dopichaj.labrador.search.hit.Hit;
import de.dopichaj.labrador.search.hit.MemoryHit;
import de.dopichaj.labrador.util.io.ArrayByteReader;
import de.dopichaj.labrador.util.io.BitInputStream;
import de.dopichaj.labrador.util.io.BitOutputStream;
import de.dopichaj.labrador.util.io.ByteReader;
import de.dopichaj.labrador.util.io.ByteWriter;


/**
 * A digest of an XML document that represents the structure at a much smaller
 * size.
 */
public final class DocumentDigest implements DocumentMetaData {

    private static final Logger log = Logger.getLogger(DocumentDigest.class);
    
    private final File file;
    private final int startId;
    private final int endId;
    private final TagFactory tagFactory;
    private final byte[] data;

    static final int OPEN = 0;
    static final int CLOSE = 1;
    static final int TEXT_LONG = 2;
    
    private enum TokenType { TT_OPEN, TT_CLOSE, TT_TEXT }

    /**
     * The length of the last TEXT token.
     */
    private int textLength;
    /**
     * The tag opened by the last OPEN token.
     */
    private Tag openedTag;
    private boolean pendingClose;
    
    static final int SMALL_MOD = 0x20;
    static final int MOD = 0x80;
    
    /**
     * Construct a DocumentDigest.
     * @param tagFactory the tag factory to use.
     * @param input the stream to read the digest from.
     * @throws IOException 
     */
    public DocumentDigest(TagFactory tagFactory, ByteReader input)
        throws IOException {

        final BitInputStream bitInput = new BitInputStream(input);
        // determine the file
        this.file = new File(new String(bitInput.readString()));
        this.startId = bitInput.readBits(31);
        log.debug("Reading digest for " + file + " (start ID: " + startId + ")");
        this.tagFactory = tagFactory;
        
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        int elementCount = copyDigest(bitInput,
            new BitOutputStream(new ByteWriter(new DataOutputStream(output), 1)));
        output.close();
        this.endId = startId + elementCount;
        this.data = output.toByteArray();
    }

    /* (non-Javadoc)
     * @see de.dopichaj.labrador.index.DocumentMetaData#getStartId()
     */
    public int getStartId() {
        return startId;
    }
    
    /* (non-Javadoc)
     * @see de.dopichaj.labrador.index.DocumentMetaData#getEndId()
     */
    public int getEndId() {
        return endId;
    }

    /* (non-Javadoc)
     * @see de.dopichaj.labrador.index.DocumentMetaData#getFile()
     */
    public File getFile() {
        return file;
    }
    
    /* (non-Javadoc)
     * @see de.dopichaj.labrador.index.DocumentMetaData#getFragments()
     */
    public Map<Integer, Hit> getFragments() {
        return getFragments(null);
    }
    
    /* (non-Javadoc)
     * @see de.dopichaj.labrador.index.DocumentMetaData#getFragments(java.util.Set)
     */
    public Map<Integer, Hit> getFragments(List<Integer> wantedIds) {
        BitInputStream in = new BitInputStream(new ArrayByteReader(data));        
        // skip the first opening tag
        try {
            TokenType type = nextToken(in);
            assert type == TokenType.TT_OPEN;
            Map<Integer, Hit> fragments = new HashMap<Integer, Hit>();
            getFragmentRecursive(fragments, wantedIds,
                new ParentContext(null, null, 1, 0), startId, in);
            return fragments;
        } catch (IOException e) {
            throw new RuntimeException("Error reading from ByteArrayInputStream", e);
        }
    }

    /**
     * 
     * @param fragments the map to store the hits in
     * @param wantedIds the IDs of the hits to include in the result, or
     *    <code>null</code> for all.
     * @param parentContext
     * @param thisId the ID of the current node
     * @param in the stream to read the digest from
     * @return the ID of the next element
     * @throws IOException
     */
    private int getFragmentRecursive(Map<Integer, Hit> fragments,
        List<Integer> wantedIds,
        ParentContext parentContext,
        Integer thisId, BitInputStream in)
        throws IOException {
        
        // determine the opening tag and XPath
        final ParentContext context =
            new ParentContext(parentContext, openedTag,
                parentContext.getPosition(openedTag), parentContext.position);
        
        // for each child
        int currentId = thisId + 1;
        TokenType type;
        do {
            type = nextToken(in);
            switch (type) {

            // if it is a text child
            case TT_TEXT:

                // update the current position and length
                context.position += textLength;
                context.length += textLength;
                break;
                
            // if it is an element child
            case TT_OPEN:
                
                // recurse
                currentId = getFragmentRecursive(fragments, wantedIds,
                    context, currentId, in);
                break;
            }
                
        // until we reach the closing tag
        } while (type != TokenType.TT_CLOSE);
        
        assert !fragments.containsKey(thisId);

        // create the hit if it is wanted
        if (wantedIds == null || wantedIds.contains(thisId)) {
            assert context.length >= 0;
            assert parentContext.position >= 0;
            fragments.put(thisId,
                new MemoryHit(file, context.getXPathParts(), context.length,
                    parentContext.position, 0.0f));
            
        }

        parentContext.length += context.length;
        parentContext.position += context.length;
        
        return currentId;
    }
    
    private static final class TagPosition {
        private final Tag tag;
        private int position;
        
        public TagPosition(final Tag tag) {
            this.tag = tag;
            this.position = 1;
        }
    }
    
    private static final class ParentContext {
        
        private List<TagPosition> positions;
        private final ParentContext grandParentContext;
        private final Tag tag;
        private final int xPathPosition;
        private int length;
        private int position;
        
        private ParentContext(ParentContext grandParentContext, Tag tag,
            int xPathPosition, int position) {
            
            this.grandParentContext = grandParentContext;
            this.tag = tag;
            this.xPathPosition = xPathPosition;
            length = 0;
            this.position = position;
        }
        
        private String getXPathPart() {
            assert tag != null;
            return tag.getName() + "[" + xPathPosition + "]";
        }
        
        private byte[] getXPathBitString() {
            
            try {
                final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
                final BitOutputStream output = new BitOutputStream(new ByteWriter(
                    new DataOutputStream(bytes), 1));
                
                // create the list of ancestors
                final List<ParentContext> ancestors = new LinkedList<ParentContext>();
                ParentContext current = this;
                while (current.grandParentContext != null) {
                    ancestors.add(0, current);
                    current = current.grandParentContext;
                }
                
                // write the depth
                output.writeUnary(ancestors.size());

                // for each ancestor, starting from the root
                for (final ParentContext ancestor : ancestors) {
                    
                    // add the tag ID and the XPath position
                    assert ancestor != null;
                    assert ancestor.tag != null;
                    output.writeGamma(ancestor.tag.getId() + 1);
                    output.writeUnary(ancestor.xPathPosition);
                }
                
                output.close();
                return bytes.toByteArray();
                
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        public String[] getXPathParts() {
            int depth = 1;
            ParentContext current = grandParentContext;
            while (current.grandParentContext != null) {
                depth++;
                current = current.grandParentContext;
            }
            
            final String[] result = new String[depth];
            current = this;
            for (int i = depth - 1; i >= 0; i--, current = current.grandParentContext) {
                result[i] = current.getXPathPart();
            }
            
            //log.debug("depth " + depth + ", bytes needed: " + getXPathBitString().length);
            
            return result;
        }

        private int getPosition(Tag tag) {
            if (positions == null) {
                positions = new ArrayList<TagPosition>();
            } else {
                for (final TagPosition tagPos : positions) {
                    if (tagPos.tag.equals(tag)) {
                        tagPos.position++;
                        return tagPos.position;
                    }
                }
            }
            
            positions.add(new TagPosition(tag));
            return 1;
        }
    }

    /**
     * Get the next token from the InputStream.
     * For an OPEN tag, {@link #openedTag} contains the tag that was opened.
     * For a TEXT tag, {@link #textLength} contains the length of the text
     * fragment.
     * @return the type of the token
     * @throws IOException
     */
    private TokenType nextToken(BitInputStream in) throws IOException {
        
        if (pendingClose) {
            pendingClose = false;
            return TokenType.TT_CLOSE;
        }
        
        final int type = in.readBits(2);
        final TokenType tokenType;
        if (type == OPEN) {
            tokenType = TokenType.TT_OPEN;

            openedTag = tagFactory.getTag(in.readGamma() - 1);
            
        } else if (type == CLOSE) {
            
            tokenType = TokenType.TT_CLOSE;
            
        } else {
            
            tokenType = TokenType.TT_TEXT;
            textLength = in.readVariableLengthInt();
        }
        
        return tokenType;
    }
    
    /**
     * Copy a digest from an input stream to an output stream.
     * @return the number of elements in the digest
     * @throws IOException
     */
    private int copyDigest(final BitInputStream bitInput,
        final BitOutputStream stream) throws IOException {
        
        int level = 0;
        int elementCount = 0;
        do {
            int type = bitInput.readBits(2);
            if (type == -1) {
                throw new IOException("Premature end of file at level " + level);
            }
            stream.writeBits(type, 2);
            if (type == OPEN) {
                elementCount++;
                level++;
                stream.writeGamma(bitInput.readGamma());
            } else if (type == CLOSE) {
                level--;
            } else if (type == TEXT_LONG) {
                stream.writeVariableLengthInt(bitInput.readVariableLengthInt());
            } else {
                assert false;
            }
        } while (level > 0);
        
        stream.close();
        return elementCount;
    }
    
}
/*
Copyright (c) 2007 Philipp Dopichaj

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/